/*******************************************************************************
 * Venice Noise Android Application
 * Copyright (C) 2011  Worcester Polytechnic Institute, Wesley Ripley
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; version 2
 * of the License
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 * If you plan to use or modify any of this code please contact Fabio Carrera (carrera.fabio@gmail.com)
 * Also please attribute any code used in the final product to the developers. 
 * 
 * Author: Wesley Ripley (wripley@wpi.edu) 
 *******************************************************************************/
package org.venicenoise.app;

import java.io.File;
import java.io.IOException;

import org.venicenoise.app.location.LocationChangedListener;
import org.venicenoise.app.location.LocationService;
import org.venicenoise.app.location.LocationUtil;
import org.venicenoise.app.location.StaticLocationService;
import org.venicenoise.app.location.TagEstimateLocationService;
import org.venicenoise.app.location.TaggedLocationService;

import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.location.Location;
import android.location.LocationManager;
import android.media.MediaPlayer;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.EditText;

/**
 * The activity where the user can record noise complaints.
 * @author Wesley Ripley
 * @version 12/15/2011
 */
public class RecordingActivity extends Activity{

	/** The requestCode used when starting the activity
	 * that takes a picture */
	private final static int PHOTO_ACTIVITY = 0;
	/**
	 * The requestCode used when staring the activity
	 * to view location settings
	 */
	private final static int LOCATION_ENABLE_ACTIVITY = 1;
	/**
	 * Id used to store a tagged location
	 */
	private final static int TAGGED_LOCATION_ID = 0;
	
	/**
	 * The length each recording should be in milliseconds
	 */
	private final static int RECORDING_LENGTH = 5000;
	
	private LocationService location;
	private LocationService cachedLocation;
	private long timeTagged = -1;
	
	private File noiseFile;
	private File photoFile;
	
	private static enum ButtonState { START, STOP };
	private ButtonState recordButtonState;
	private ButtonState playButtonState;
	
	private RecordAudio recorder;
	private MediaPlayer mediaPlayer;
	
	private SubmissionQue submissionQue;
	
	private ProgressDialog waitForLocationDialog;
	
	/**
	 * Whether or not we should only submit when WiFi is available
	 */
	private boolean wifiOnly = false;
	/**
	 * Whether or not we are in field test mode;
	 */
	private boolean fieldTestMode = false;
	
	/**
	 * An onCancelListener that will move the user back to the main activity, exiting them from this
	 * activity
	 */
	private DialogInterface.OnCancelListener backToMainOnCancel = new DialogInterface.OnCancelListener() {
		
		public void onCancel(DialogInterface dialog) {
			dialog.dismiss();
			goBackToMain();
		}
	};
	/**
	 * For testing purposes. Used when AppUtil.FIX_LOCATION is true
	 */
	private StaticLocationService fixedLocation = new StaticLocationService(45.425799,12.322812,0.1f,90.0f);
	
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		this.requestWindowFeature(Window.FEATURE_NO_TITLE);
		
		SharedPreferences prefs = PreferenceManager
                .getDefaultSharedPreferences(getBaseContext());
		//check whether we are in field test mode or not
		fieldTestMode = prefs.getBoolean(getString(R.string.pref_fieldTestingKey), false);
		//media player for play button
		mediaPlayer = new MediaPlayer();
		mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
			
			public void onCompletion(MediaPlayer mp) {				
				audioPlaybackDone();
			}
		});
		
		// Acquire a reference to the system Location Manager
		final LocationManager locationManager = getLocationManager();
		if(!AppUtil.FIX_LOCATION)
		{
			//start acquiring location
			location = new TagEstimateLocationService(locationManager);
			location.startLocating();
			//inform user if GPS not enabled
			if(!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER))
			{
				int numProviders = LocationUtil.numAvailableLocationProviders(locationManager);
				
				DialogInterface.OnClickListener onSkip = new DialogInterface.OnClickListener(){
					//code that runs if the skip button is pressed
					public void onClick(DialogInterface dialog, int which) {
						dialog.dismiss();
					}
				};
				boolean gpsOnly = numProviders > 0;
				Dialog enableLocationDialog = EnableLocationDialog.makeDialog(gpsOnly, gpsOnly ? null : backToMainOnCancel, onSkip, this, LOCATION_ENABLE_ACTIVITY);
				enableLocationDialog.show();//see onActivityResult for code that runs after the user comes back from location settings
			}
		}
		else
		{
			location = fixedLocation;
		}
		cachedLocation = null;
		resetState();
		AppUtil.setVersionInHeader(this);
		restoreState();

		submissionQue = new SubmissionQue(this);
	}
	
	/** Called when the activity is becoming visible to the user. */
	@Override
	public void onStart()
	{
		super.onStart();
		SharedPreferences prefs = PreferenceManager
                .getDefaultSharedPreferences(getBaseContext());
		//check wifi only preference
		wifiOnly = prefs.getBoolean(getString(R.string.pref_wifiOnlyKey), false);
	}
	
	/** Called when the system is about to start resuming a previous activity. */
	@Override
	public void onPause()
	{
		super.onPause();
		//we are moving away from this activity so save the application state
		saveState();
	}
	
	/** The final call you receive before your activity is destroyed */
	@Override
	public void onDestroy()
	{
		super.onDestroy();
		submissionQue.close();
		submissionQue = null;
		location.stopLocating();
		location = null;
		if(cachedLocation != null)
		{
			cachedLocation.stopLocating();
		}
		cachedLocation = null;
	}
	/**
	 * Reset the state of the application.
	 */
	private void resetState()
	{
		if(fieldTestMode)
		{
			//use special layout for field test mode
			setContentView(R.layout.recording_fieldtest);
			if(AppUtil.FIX_LOCATION)
			{
				//notify user that the location is fixed
				View indicator = findViewById(R.id.testingIndicator);
				if(indicator != null)
				{
					indicator.setVisibility(View.VISIBLE);
				}
			}
			autoIncrementDescription();
		}
		else
		{
			setContentView(R.layout.recording);
		}
		noiseFile = null;
		photoFile = null;
        recordButtonState = ButtonState.START;
        playButtonState = ButtonState.START;
        resetTag(); //reset the location
	}
	
	/**
	 * Auto-increment the description based on the last point queued.
	 * 
	 * If the description of the last point queued has a number as its first
	 * word, the description of this point is set to 1+ that number.
	 * Useful for field recording.
	 */
	private void autoIncrementDescription()
	{
		final EditText description = (EditText)findViewById(R.id.complaintBox);
		if(description != null && submissionQue != null)
		{
			NoiseData lastSubmitted = submissionQue.getLastSubmitted();
			if(lastSubmitted != null)
            {
                String text = lastSubmitted.getDescription();
                if(text.contains(" "))//if there is more than one word
                {
                	//just take the first word
                	text = text.substring(0, text.indexOf(' '));
                }    
				try
				{
                    int counter = Integer.parseInt(text) + 1;
                    description.setText(counter + description.getText().toString());
                }catch(NumberFormatException e){
                        //do nothing on purpose, if it couldnt parse dont worry about it
                }
            }
		}
	}
	
	/**
	 * Called when the record button is clicked 
	 * (set as onClick for the record button)
	 * @param v
	 * 		The view that was clicked
	 */
	public void recordButtonClicked(View v)
	{
		final Button recordButton = ((Button)v);
		final View playButton = findViewById(R.id.playButton);
		//if we have not started recording
		if(recordButtonState == ButtonState.START)
		{
			if(noiseFile == null)
			{
				//create the audio file if it doesn't exist already
				noiseFile = AppUtil.createFile(this,"recording",".wav",true);
			}
			recorder = new RecordAudio(noiseFile);
			recorder.startRecoding();
			recordButtonState = ButtonState.STOP;
			recordButton.setText(R.string.stopRecord);
			//disable both record button and play button during recording
			recordButton.setEnabled(false);
			playButton.setEnabled(false);
			//limit the recording to the given length
			final Handler handler = new Handler();
			handler.postDelayed(new Runnable() {
			    public void run() {
			    	//this is called once the time given bellow has passed
			    	if(recorder.isRecording())
			    	{
			    		stopRecording();
			    	} 
			    }
			}, RECORDING_LENGTH);
		}
		//if we are already recording
		else if(recordButtonState == ButtonState.STOP)
		{
			//then stop the recording
			stopRecording();
		}
	}
	/**
	 * Stop the audio recording
	 */
	private void stopRecording()
	{
		final Button recordButton = (Button)findViewById(R.id.recordButton);
		final View playButton = findViewById(R.id.playButton);
		recorder.stopRecording();
		//reset record button state
		recordButtonState = ButtonState.START;
		recordButton.setText(R.string.record);
		//enable both record and play
		recordButton.setEnabled(true);
		playButton.setEnabled(true);
		//tag the location, recording where we took this noise recording
		tagLocation();
	}
	
	/**
	 * Called when play button is clicked 
	 * (set as onCick for play button)
	 * @param v
	 * 		The view that was clicked
	 */
	public void playButtonClicked(View v)
	{
		//if we are not playing anything
		if(playButtonState == ButtonState.START)
		{
			final Button playButton = (Button)v;
			final View recordButton = findViewById(R.id.recordButton);
			try 
			{
				//play the noise file
				mediaPlayer.reset();
				mediaPlayer.setDataSource(this,Uri.fromFile(noiseFile));
				mediaPlayer.prepare();
				mediaPlayer.start();
				//change the play button state
				playButtonState = ButtonState.STOP;
				playButton.setText(R.string.stopPlay);
				//disable recording while playing
				recordButton.setEnabled(false);
				
			} catch (IllegalArgumentException e) {
				if(AppUtil.LOGGING)
				{
					Log.e(AppUtil.LOG_TAG,"IllegalArgumentException when trying to play noise file");
				}
				AppUtil.showErrorDialog(this, getString(R.string.cannotPlay));
			} catch (SecurityException e) {
				if(AppUtil.LOGGING)
				{
					Log.e(AppUtil.LOG_TAG,"SecurityException when trying to play noise file");
				}
				AppUtil.showErrorDialog(this, getString(R.string.cannotPlay));
			} catch (IllegalStateException e) {
				if(AppUtil.LOGGING)
				{
					Log.e(AppUtil.LOG_TAG,"IllegalStateException when trying to play noise file");
				}
				AppUtil.showErrorDialog(this, getString(R.string.cannotPlay));
			} catch (IOException e) {
				if(AppUtil.LOGGING)
				{
					Log.e(AppUtil.LOG_TAG,"IOException when trying to play noise file");
				}
				AppUtil.showErrorDialog(this, getString(R.string.cannotPlay));
			}
		}
		//if we are already playing
		else if(playButtonState == ButtonState.STOP)
		{
			//stop playback
			audioPlaybackDone();
		}
	}
	
	/**
	 * Stop playing
	 */
	private void audioPlaybackDone()
	{
		final Button playButton = (Button)findViewById(R.id.playButton);
		final View recordButton = findViewById(R.id.recordButton);
		mediaPlayer.stop();
		//reset play button state
		playButtonState = ButtonState.START;
		playButton.setText(R.string.play);
		//reenable record button
		recordButton.setEnabled(true);
	}
	
	/**
	 * Called when photo button clicked.
	 * (set as onClick for photo button)
	 * @param v
	 * 		The view that was clicked
	 */
	public void photoButtonClicked(View v)
	{  
		//create the photo file
		photoFile = AppUtil.createFile(this, "photo",".jpg",false); 
		final Uri imageFileUri = Uri.fromFile(photoFile);
		//start an activity to take a photo 
		final Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
		//tell it to store the picture in the photo file
		i.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri); 
		startActivityForResult(i, PHOTO_ACTIVITY); //see onActivityResult for what happens when this activity returns
	}
	
	/**
	 * Called when view photo button clicked
	 * (set as onClick for view photo button)
	 * @param v
	 * 		The view that was clicked
	 */
	public void viewPhotoButtonClicked(View v)
	{
		//if we have taken a photo
		if(photoFile != null)
		{
			//start an activity to view the photo
			Intent intent = new Intent();
			intent.setAction(android.content.Intent.ACTION_VIEW);
			intent.setDataAndType(Uri.fromFile(photoFile), "image/jpg");
			startActivity(intent);
		}
		else
		{
			AppUtil.showErrorDialog(this, getString(R.string.cannotViewPhoto));
		}
	}
	
	/**
	 * Called when an activity you launched exits
	 * @param requestCode
	 * 		the request code given when starting the activity
	 * @param resultCode
	 * 		The result code returned by the child activity through its setResult().
	 * @param data	
	 * 		An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
	 */
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent intent) 
	{        
	    super.onActivityResult(requestCode, resultCode, intent);
	    //if user returning from photo activity
	    if (requestCode == PHOTO_ACTIVITY)
	    {
	    	//if the picture did not take okay.
	    	if(resultCode != RESULT_OK || !photoFile.isFile())
	    	{
	    		//make sure the file is null
	    		photoFile = null;
	    		AppUtil.showErrorDialog(this, getString(R.string.cannotTakePhoto));
	    	}
	    	else
	    	{
	    		//enable the view button
	    		View viewPhotoButton = findViewById(R.id.viewPhotoButton);
	    		viewPhotoButton.setEnabled(true);
	    	}
	    }
	    //if user returning from location settings
	    else if(requestCode == LOCATION_ENABLE_ACTIVITY)
	    {
	    	LocationManager locationManager = getLocationManager();
	    	int numProviders = LocationUtil.numAvailableLocationProviders(locationManager);
	    	//make sure at least one provider is enabled
	    	if(numProviders < 1)
	    	{
	    		//cannot proceed with no location, go back to main activity
	    		goBackToMain();
	    		return;
	    	}
	    }
	}
	
	/**
	 * Return to the main activity, closing this one
	 */
	private void goBackToMain()
	{
		startActivity(new Intent(this, MainActivity.class));
		finish();
		return;
	}
	
	/**
	 * Called when submit button clicked
	 * (set as onClick for submit button)
	 * @param v
	 * 		The view that was clicked
	 */
	public void submitButtonClicked(View v)
	{
		//queue the data
		boolean sucessfullyQued = queData();
		if(!sucessfullyQued)//if we couldnt queue it
		{
			//do nothing
			return;
		}
		final ConnectivityManager connManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
		final NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
		final NetworkInfo mobile = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
		//if we have connection to submit
		if(wifi.isConnected() || (!wifiOnly && mobile.isConnected()))
		{
			//submit the data to the web in the background
			SubmitData submitter = new SubmitData(this,new SubmitData.PostExecutable(){
				public void onPostExecute() {
					//runs after submitting is finished
					afterDataSaved();
				} 
			},true);
			submitter.execute(submissionQue);
		}
		else
		{
			//no connection, inform user data only queued not submitted
			AppUtil.showInfoDialog(this, getString(R.string.submissionQueued), getString(R.string.quedMessage));
			afterDataSaved();
		}
	}
	
	/**
	 * Called after queue button is clicked
	 * (Set as onClick for queue button)
	 * @param v
	 * 		The view that was clicked
	 */
	public void queButtonClicked(View v)
	{
		//queue data
		boolean sucessfullyQued = queData();
		if(!sucessfullyQued)
		{
			//do nothing
			return;
		}
		//inform user data queued
		AppUtil.showInfoDialog(this, "", getString(R.string.submissionQueued));
		afterDataSaved();
	}
	
	private void afterDataSaved()
	{
		//reset state so we can record again
		resetState();
		//save state to clear any previously saved state
		saveState();
	}
	
	/**
	 * Put the data the user has entered into the queue
	 * @return
	 * 		true if data is successfully added to queue, false otherwise
	 */
	private boolean queData()
	{
		final EditText complaintBox = (EditText)findViewById(R.id.complaintBox);
		//cannot queue with no location
		if(!location.locationAvailable())
		{
			AppUtil.showErrorDialog(this, getString(R.string.noLocation));
			return false;
		}
		double leq = 0;
		//get the leq if this is field test mode
		if(fieldTestMode)
		{
			EditText leqBox = (EditText)findViewById(R.id.leqBox);
			if(leqBox != null)
			{
				String leqText = leqBox.getText().toString();
				if(AppUtil.isEmptyOrNull(leqText))
				{
					leq = 0;
				}
				else
				{
					try{
						leq = Double.parseDouble(leqText);
					}catch(NumberFormatException e)
					{
						//if leqBox text is not formatted right ignore it
						leq = 0;
					}
				}
			}
		}
		//cannot queue without a recorded noise file
		if(noiseFile == null)
		{
			AppUtil.showErrorDialog(this, getString(R.string.submitNoRecord));
			return false;
		}
		//cannot queue without timestamp
		if(timeTagged < 0)
		{
			AppUtil.showErrorDialog(this,getString(R.string.cannotSubmit));
			return false;
		}
		NoiseData.Type type = AppUtil.FIX_LOCATION ? NoiseData.Type.TEST : AppUtil.MODE;
		//if we have taken a photo
		if(photoFile != null)
		{
			//scale image down
			Bitmap scaledImage = ImageUtil.scaleImageFile(photoFile, 800);
			if(!ImageUtil.compressImageFile(scaledImage, photoFile, CompressFormat.JPEG, 80))
			{
				//if we couldn't write the image file, dont queue it
				photoFile = null;
			}
		}
		//add data to queue
		final NoiseData data = new NoiseData(location, 0,leq,0, complaintBox.getText().toString(),
											noiseFile, photoFile,timeTagged/1000L,type);
		submissionQue.addSubmission(data);
		return true;
	}
	
	/**
	 * Get the systems location manager
	 * @return
	 * 		The location manager
	 */
	private LocationManager getLocationManager()
	{
		return (LocationManager) getSystemService(Context.LOCATION_SERVICE);
	}
	
	/**
	 * Tag the location where the noise was recorded.
	 */
	private void tagLocation()
	{
		//if we don't have a location
		if(!location.locationAvailable())
		{
			//make user wait for location
			location.addLocationListener(new LocationAvailableListener());
			waitForLocationDialog = new ProgressDialog(this);
			waitForLocationDialog.setMessage(getString(R.string.findingLocation));
			waitForLocationDialog.setIndeterminate(true);
			waitForLocationDialog.setCancelable(true);
			//if they cancel make them so back to the main activity
			waitForLocationDialog.setOnCancelListener(backToMainOnCancel);
			waitForLocationDialog.show();
		}
		else
		{
			//set the time the location was tagged
			timeTagged = System.currentTimeMillis();
			//cache the old location service so it still keeps getting location updates
			cachedLocation = location;
			//switch location to a tagged location
			location = new TaggedLocationService(getLocationManager(),cachedLocation.getLocation(),TAGGED_LOCATION_ID);
			location.startLocating();
		}
	}
	/**
	 * Reset location tag so location can be anywhere
	 */
	private void resetTag()
	{
		//reset time tagged
		timeTagged = -1;
		if(cachedLocation != null)
		{
			//restore location from cache if possible
			location = cachedLocation;
			cachedLocation = null;
		}
		else
		{
			if(AppUtil.FIX_LOCATION)
			{
				location = fixedLocation;
			}
			else
			{
				location = new TagEstimateLocationService(getLocationManager());
			}
		}
		location.startLocating();
	}
	
	 /**
	  * Small inner class that dismisses the waiting for location dialog, when a location becomes available
	  * and then stops listening for updates 
	  * @author Wesley Ripley
	  */
	private class LocationAvailableListener extends LocationChangedListener
	{
		public void onLocationChanged(Location newLocation) {
			if(waitForLocationDialog != null)
			{
				//when the location changes dismiss the dialog
				waitForLocationDialog.dismiss();
				location.removeLocationListener(this);
				//and tag the location
				tagLocation();
			}
		}
	}
	
	//the constants for the keys used to store state in SharedPreferences
	private static final String KEY_DESC = "description";
	private static final String KEY_AUDIO = "audiofile";
	private static final String KEY_PHOTO = "photofile";
	private static final String KEY_LOCATION = "locationSaved";
	private static final String KEY_TIME = "timeTagged";
	
	/**
	 * Save the state of this activity
	 * @return
	 * 		true is successfully saved, false otherwise
	 */
	private boolean saveState()
	{
		SharedPreferences.Editor prefEditor = getPreferences(MODE_PRIVATE).edit();
		if(prefEditor == null)
		{
			return false;
		}
		//save description
		EditText descriptionBox = (EditText)findViewById(R.id.complaintBox);
		if(descriptionBox != null)
		{
			prefEditor.putString(KEY_DESC,descriptionBox.getText().toString());
		}
		else
		{
			prefEditor.remove(KEY_DESC);
		}
		//save photo
		if(photoFile != null)
		{
			prefEditor.putString(KEY_PHOTO, photoFile.getAbsolutePath());
		}
		else
		{
			prefEditor.remove(KEY_PHOTO);
		}
		//if we are in a good, tagged state
		if(timeTagged > 0 && noiseFile != null && location != null && 
				location instanceof StateSaveable && location.locationAvailable())
		{
			//save audio file, location, and time
			prefEditor.putString(KEY_AUDIO, noiseFile.getAbsolutePath());
			((StateSaveable)location).saveSate(prefEditor);
			prefEditor.putBoolean(KEY_LOCATION, true);
			prefEditor.putLong(KEY_TIME, timeTagged);
		}
		else
		{
			prefEditor.remove(KEY_AUDIO);
			prefEditor.remove(KEY_LOCATION);
			prefEditor.remove(KEY_TIME);
		}
		
		return prefEditor.commit();
	}
	
	/**
	 * Restore saved application state
	 */
	private void restoreState() {
		SharedPreferences prefs = getPreferences(MODE_PRIVATE);
		EditText descriptionBox = (EditText)findViewById(R.id.complaintBox);
		//load description
		if(descriptionBox != null)
		{
			descriptionBox.setText(prefs.getString(KEY_DESC,""));
		}
		//load audio file
		if(prefs.contains(KEY_AUDIO))
		{
			View playButton = findViewById(R.id.playButton);
			if(playButton != null)
			{
				noiseFile = new File(prefs.getString(KEY_AUDIO, null));
				playButton.setEnabled(true);
			}
		}
		//load photo
		if(prefs.contains(KEY_PHOTO))
		{
			photoFile = new File(prefs.getString(KEY_PHOTO, null));
			View viewPhotoButton = findViewById(R.id.viewPhotoButton);
			viewPhotoButton.setEnabled(true);
		}
		//load location
		if(prefs.getBoolean(KEY_LOCATION,false))
		{
			cachedLocation = location;
			location = new TaggedLocationService(getLocationManager(),prefs,TAGGED_LOCATION_ID);
			location.startLocating();
		}
		//load timestamp
		timeTagged = prefs.getLong(KEY_TIME, -1);
	}
}